package org.example.controller;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import org.example.uitls.ResultData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.*;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 类加载器读取：读取类路径下任意位置中的任意资源.
 * 即便是打包成 .jar、.war 包部署到生产环境，照样读取正常。
 *
 * @author wangMaoXiong
 * @version 1.0
 * @date 2021/4/2 19:29
 */
@RestController
public class ClassLoaderController {

    private static final Logger log = LoggerFactory.getLogger(ClassLoaderController.class);

    /**
     * http://localhost:8080/classLoader/URL2File
     * 1、ClassLoader.getResource 方法内部使用 utf-8 对路径信息进行了编码，当路径中存在中文或空格等字符时，就会对它们进行转换，
     * 这样获取路径信息时就会出现 utf-8 编码后的字符，如 '85%ac%e5%bc%80%e9%85%8d%e7%bd'，看起以来就像乱码一样。
     * 2、解决乱码的办法是使用 URLDecoder.decoder 方法对地址进行解码，以得到原始路径。
     *
     * @throws UnsupportedEncodingException
     */
    @GetMapping("classLoader/URL2File")
    public ResultData URL2File() throws Exception {
        List<String> list = new ArrayList<>(8);
        // String classPath = "static/测试单位-001001_多表头.xlsx";
        String classPath = "static/index.html";
        URL url = ClassLoaderController.class.getClassLoader().getResource(classPath);
        String path = url.getPath();

        // "/D:/project/IDEA_project/java-se/target/classes/static/%e6%b5%8b%e8%af%95%e5%8d%95%e4%bd%8d-001001_%e5%a4%9a%e8%a1%a8%e5%a4%b4.xlsx,false",
        list.add(path + "," + new File(path).exists());

        // "file:/D:/project/IDEA_project/java-se/target/classes/static/%e6%b5%8b%e8%af%95%e5%8d%95%e4%bd%8d-001001_%e5%a4%9a%e8%a1%a8%e5%a4%b4.xlsx,false",
        list.add(url.toString() + "," + new File(url.toString()).exists());

        // "/D:/project/IDEA_project/java-se/target/classes/static/%e6%b5%8b%e8%af%95%e5%8d%95%e4%bd%8d-001001_%e5%a4%9a%e8%a1%a8%e5%a4%b4.xlsx,false",
        list.add(url.getFile() + "," + new File(url.getFile()).exists());

        // "/D:/project/IDEA_project/java-se/target/classes/static/测试单位-001001_多表头.xlsx",
        list.add(URLDecoder.decode(path, "utf-8"));

        // "D:\\project\\IDEA_project\\java-se\\target\\classes\\static\\测试单位-001001_多表头.xlsx,true"
        list.add(new File(URLDecoder.decode(path, "utf-8")).getAbsolutePath() + "," + new File(URLDecoder.decode(path, "utf-8")).exists());

        return ResultData.success(list);
    }

    /**
     * http://localhost:8080/classLoader/read1
     * <p>
     * URL getResource(String name) : 查找类路径下指定名称的资源
     * 1、name ：只要是编译后位于 classed 目录下的任意文件都行，多级时用左斜杠即可，如 config/root/data.xml
     * 2、通常用于读取 resources 资源目录下的资源，然而其实 src/java 源码目录的文件同样可以，只要编译后能到 classes 目录下
     * 3、注意只能是类路径下的，不能是其它位置，也不能是绝对路径，不存在时 getResource 返回 null。
     */
    @GetMapping("classLoader/read1")
    public ResultData classLoader1() {
        Map<String, Object> dataMap = new HashMap<>(8);
        // String classPath = "config/config2.properties";
        String classPath = "config/配置说明.setting";
        URL url = ClassLoaderController.class.getClassLoader().getResource(classPath);
        Assert.notNull(url, "资源文件不存在！");
        String path1 = url.getPath();
        dataMap.put("path", path1);

        List<String> list = FileUtil.readUtf8Lines(url);
        dataMap.put("content", list);
        return ResultData.success(dataMap);
    }

    /**
     * http://localhost:8080/classLoader/read2
     * <p>
     * InputStream getResourceAsStream(String name) ：读取类路径下知道名称的资源，并返回字节输入流
     * 其实底层就是在 getResource 的基础上，加了一个  url.openStream()，所以资源不存在时，返回 null。
     * 注意只能是类路径下的，不能是其它位置，也不能是绝对路径。
     * <p>
     * 使用流的好处是可以读取任何文件的内容，比如图片、音视频等等。
     * 如果是文本文件则推荐使用其它更加简易快捷的方式。cn.hutool.core.io.resource.ResourceUtil
     * <p>
     * 即便是打包成 .jar、.war 包部署到生产环境，照样读取正常。
     */
    @GetMapping("classLoader/read2")
    public ResultData classLoader2() throws IOException {
        List<String> dataList = new ArrayList<>();
        // String classPath = "config/config2.properties";
        String classPath = "config/配置说明.setting";
        try (InputStream inputStream = ClassLoaderController.class.getClassLoader().getResourceAsStream(classPath)) {
            dataList = IoUtil.readUtf8Lines(inputStream, dataList);
            return ResultData.success(dataList);
        }
    }

    /**
     * http://localhost:8080/resource/read1
     * <p>
     * org.springframework.core.io.ClassPathResource(String path)：类路径资源文件，读取类路径下下的任意资源文件，如果路径有前缀 / 也会自动被去掉，
     * <pre>
     *  org.springframework.core.io.Resource 有以下常用的方法：
     *      boolean exists = resource.exists();
     *      InputStream inputStream1 = resource.getInputStream();
     *      File file = resource.getFile();
     *      String filename = resource.getFilename();
     *      URI uri = resource.getURI();
     *      URL url = resource.getURL();
     *      long l = resource.contentLength();
     *      boolean file1 = resource.isFile();
     *      long l1 = resource.lastModified();
     * </pre>
     * 即便是打包成 .jar、.war 包部署到生产环境，照样读取正常。
     *
     * @return
     */
    @GetMapping("resource/read1")
    public ResultData resource1() throws IOException {
        Resource resource = new ClassPathResource("application.yml");
        try (InputStream inputStream = resource.getInputStream()) {
            String readUtf8 = IoUtil.readUtf8(inputStream);
            return ResultData.success(readUtf8);
        }
    }

    /**
     * http://localhost:8080/resourceUtils/read1
     * <p>
     * org.springframework.util.ResourceUtils：用于将资源位置解析为文件系统中的文件的实用程序方法。主要用于框架内部使用。
     * 不推荐使用！！！
     * 不推荐使用！！！
     * 不推荐使用！！！
     * JAR 包内部的文件无法获取，必须是一个独立的文件，即项目打包后，无法再获取包中的文件。
     * <p>
     * File getFile(String resourceLocation)：将给定的资源位置解析为文件系统中的文件。不检查文件是否实际存在；只需返回给定位置对应的文件。
     * * resourceLocation：要解析的资源位置："classpath:" 伪URL，类路径资源；"file:" URL或纯文件路径
     * File getFile(URI resourceUri)：将给定的资源URI解析为文件系统中的文件。
     * File getFile(URL resourceUrl)：将给定的资源URL解析为文件系统中的文件。
     * URL extractJarFileURL(URL jarUrl):从给定的URL中提取实际jar文件的URL（可能指向jar文件中的资源或jar文件本身）。
     *
     * @return
     */
    @GetMapping("resourceUtils/read1")
    public ResultData resourceUtils1() throws FileNotFoundException {
        File file = ResourceUtils.getFile("classpath:application.yml");
        List<String> list = FileUtil.readUtf8Lines(file);
        return ResultData.success(list);
    }

    /**
     * Spring 框架提供了 Resource 接口和 ResourceLoader 接口来方便地访问资源文件
     * http://localhost:8080/resourceLoader/read1
     * <p>
     * 即便是打包成 .jar、.war 包部署到生产环境，照样读取正常。
     */
    @javax.annotation.Resource
    private ResourceLoader resourceLoader;

    @GetMapping("resourceLoader/read1")
    public ResultData resourceLoader() throws Exception {
        String classPath = "classpath:config/配置说明.setting";
        Resource resource = resourceLoader.getResource(classPath);
        try (InputStream inputStream = resource.getInputStream();
             InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
             BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
            List<String> readLines = new ArrayList<>();
            readLines = IoUtil.readLines(bufferedReader, readLines);
            return ResultData.success(readLines);
        }
    }

    /**
     * 可以直接使用 getClass().getResourceAsStream() 方法，它 会返回一个 InputStream 对象。
     * 如果此对象是由引导类加载器加载的，则该方法将委托给{@link ClassLoader#getSystemResourceAsStream}。
     * http://localhost:8080/class/getResourceAsStream
     * <p>
     * 即便是打包成 .jar、.war 包部署到生产环境，照样读取正常。
     *
     * @return
     * @throws Exception
     */
    @GetMapping("class/getResourceAsStream")
    public ResultData getResourceAsStream() throws Exception {
        List<String> readLines = new ArrayList<>();
        // 类路径下的文件时必须以"/"开头.
        String classPath = "/config/配置说明.setting";
        InputStream inputStream = getClass().getResourceAsStream(classPath);
        if (inputStream != null) {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
                readLines = IoUtil.readLines(bufferedReader, readLines);
            }
        } else {
            return ResultData.error("读取类路径下文件失败：" + classPath);
        }
        return ResultData.success(readLines);
    }

    /**
     * Spring 框架的 StreamUtils 类提供了一个便捷的方法来直接读取资源内容到字符串
     * http://localhost:8080/streamUtils/copyToString
     * <p>
     * 即便是打包成 .jar、.war 包部署到生产环境，照样读取正常。
     *
     * @return
     * @throws Exception
     */
    @GetMapping("streamUtils/copyToString")
    public ResultData streamUtils() throws Exception {
        String classPath = "config/配置说明.setting";
        Resource resource = new ClassPathResource(classPath);
        String copyToString = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
        return ResultData.success(copyToString);
    }

    /**
     * Spring 的 ApplicationContext 提供了一种访问资源的方式，它允许你按名称查找资源。
     * http://localhost:8080/applicationContext/getResource
     * <p>
     * 即便是打包成 .jar、.war 包部署到生产环境，照样读取正常。
     * <p>
     * 直接从容器中获取 ApplicationContext 实例即可，默认已经有了。
     */
    @javax.annotation.Resource
    private ApplicationContext applicationContext;

    @GetMapping("applicationContext/getResource")
    public ResultData applicationContext() throws Exception {
        List<String> readLines = new ArrayList<>();
        String classPath = "classpath:config/配置说明.setting";
        Resource resource = applicationContext.getResource(classPath);
        try (InputStream inputStream = resource.getInputStream()) {
            readLines = IoUtil.readLines(inputStream, Charset.forName("UTF-8"), readLines);
        }
        return ResultData.success(readLines);
    }

    /**
     * http://localhost:8080/classLoader/listFiles2
     * PathMatchingResourcePatternResolver-资源模式解析器 批量读取类路径文件资源
     * 使用场景1：读取服务包中指定目录下的所有文件信息，比如想读取整个服务中的所有 .html 文件信息。
     * <p>
     * 即便是打包成 .jar、.war 包部署到生产环境，照样读取正常。
     *
     * @return
     * @throws Exception
     */
    @GetMapping("classLoader/listFiles2")
    public ResultData listFiles2() throws Exception {
        List<Map<String, Object>> fileInfos = new ArrayList<>();
        // 创建 资源模式解析器 对象，ClassLoader 访问将通过线程上下文类加载器进行
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 将给定的位置模式解析为 Resource 资源对象（包括目录与文件）。
        // /*：表示直接子级(不包括孙级)、/**：表示递归下面的所有子孙级资源
        // 示例1：static/**：表示类路径下static目录下面的全部文件，包括子孙文件
        // 示例2：static/**/*.html：表示类路径下static目录下面所有的html文件，包括子孙文件
        // 示例3：**/cors/**/*.html：表示类路径下，路径包含cors的html文件
        // 示例4：**/*.html：表示类路径下全部的html文件
        // 示例5：*.*：表示类路径根目录下的全部文件，不包括子目录下的文件
        // 示例6：**/*.*：表示类路径下的全部文件，包括子孙文件
        Resource[] resources = resolver.getResources("static/**");
        for (Resource resource : resources) {
            Map<String, Object> fileInfo = new HashMap<>(16);
            // 确定此资源的文件名，即通常是路径的最后一部分：例如“myfile.txt";如果此类资源没有文件名，则返回 null;
            fileInfo.put("fileName", resource.getFilename());
            // 返回此资源的描述;
            fileInfo.put("description", resource.getDescription());
            // 获取资源的绝对路径，并防止中文乱码
            fileInfo.put("absolutePath", URLDecoder.decode(resource.getURL().getPath(), "utf-8"));
            if (StrUtil.endWithIgnoreCase(resource.getFilename(), "html")) {
                // 如果是 html 文件，则读取它的 title 值
                String readUtf8 = IoUtil.readUtf8(resource.getInputStream());
                String title = readUtf8.substring(readUtf8.indexOf("<title>") + 7, readUtf8.indexOf("</title>"));
                // title=CSRF 跨站请求
                // title=蚩尤后裔
                System.out.println("title=" + title);
            }
            fileInfos.add(fileInfo);
        }
        // 下面是 IDEA 直接访问的效果
        //   {
        //       "fileName": "cors",
        //       "description": "file [D:\\project\\IDEA_project\\java-se\\target\\classes\\static\\cors]",
        //       "absolutePath": "/D:/project/IDEA_project/java-se/target/classes/static/cors/"
        //     },
        //   {
        //       "fileName": "cors_filter.html",
        //       "description": "file [D:\\project\\IDEA_project\\java-se\\target\\classes\\static\\cors\\cors_filter.html]",
        //       "absolutePath": "/D:/project/IDEA_project/java-se/target/classes/static/cors/cors_filter.html"
        //     }

        // 下面是打成 jar 后访问的效果
        //     {
        //       "fileName": "",
        //       "description": "class path resource [static/]",
        //       "absolutePath": "file:/D:/project/IDEA_project/java-se/target/java-se-1.0-SNAPSHOT.jar!/BOOT-INF/classes!/static/"
        //     },
        //     {
        //       "fileName": "",
        //       "description": "class path resource [static/cors/]",
        //       "absolutePath": "file:/D:/project/IDEA_project/java-se/target/java-se-1.0-SNAPSHOT.jar!/BOOT-INF/classes!/static/cors/"
        //     },
        //     {
        //       "fileName": "cors_filter.html",
        //       "description": "class path resource [static/cors/cors_filter.html]",
        //       "absolutePath": "file:/D:/project/IDEA_project/java-se/target/java-se-1.0-SNAPSHOT.jar!/BOOT-INF/classes!/static/cors/cors_filter.html"
        //
        return ResultData.success(fileInfos);
    }

}
